Skip to main content

Errors and Exceptions

The Try-Except Construct

Exception handling in Python allows programs to handle errors gracefully, ensuring that unexpected situations do not crash the program.

Understanding Try-Except Blocks

The try-except construct lets you test a block of code for errors and handle them accordingly.

Syntax

try:
risky_operation()
except SpecificException:
handle_exception()
except AnotherException:
handle_another_exception()
else:
proceed_normally()
finally:
cleanup_operations()

Description

  • Execution Flow:
    1. try Block: Executes first.
    2. except Blocks: If an exception occurs, the first matching except block is executed.
    3. else Block: Executes if no exceptions were raised.
    4. finally Block: Executes regardless of what happened earlier.

Examples

Example 1: Basic try-except

try:
number = int(input("Enter a number: "))
reciprocal = 1 / number
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")

Example 2: Using else

try:
with open('data.txt', 'r') as file:
data = file.read()
except FileNotFoundError:
print("File not found.")
else:
print("File read successfully!")

Example 3: Using finally

try:
file = open('data.txt', 'r')
data = file.read()
except IOError:
print("Error reading file.")
finally:
file.close()
print("File closed.")

Example 4: Combining try, except, else, and finally

try:
file = open('data.txt', 'r')
data = file.read()
except FileNotFoundError:
print("File not found.")
except IOError:
print("Error reading file.")
else:
print("File read successfully!")
finally:
file.close()
print("File closed.")

Example 5: Raising an Exception

def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative.")
print(f"Age set to {age}")

try:
set_age(-5)
except ValueError as e:
print(e)

Example 6: Custom Exception

class InsufficientFundsError(Exception):
pass

def withdraw(amount, balance):
if amount > balance:
raise InsufficientFundsError("Insufficient funds for withdrawal.")
return balance - amount

try:
new_balance = withdraw(150, 100)
except InsufficientFundsError as e:
print(e)
  • try Block: Encapsulates code that may raise an exception.
  • except Clause: Handles specific exceptions that occur in the try block.
  • else Clause: Executes if no exceptions are raised in the try block.
  • finally Clause: Executes code regardless of whether an exception was raised.
  • raise Statement: Manually triggers an exception, allowing for custom error signaling.

Example: Handling File I/O Errors

Consider a function that counts the frequency of each character in a file:

def character_frequency(filename):
"""Counts the frequency of each character in the given file."""
# First try to open the file
try:
f = open(filename)
except OSError:
return None
# Now process the file
characters = {}
for line in f:
for char in line:
characters[char] = characters.get(char, 0) + 1
f.close()
return characters
  • Try Block: Attempts to open the file, which might raise an OSError.
  • Except Block: Returns None if an OSError occurs, indicating the file couldn't be opened.
  • Processing: If no exception, counts character frequency in the file.

When to Use Try-Except

  • File Operations: Opening, reading, or writing files that may not exist or be inaccessible.
  • Type Conversions: Converting data types that might fail (e.g., string to integer).
  • External Resources: Interacting with network services or system commands that might fail.

Best Practices

  • Specific Exceptions: Catch specific exceptions rather than a general Exception to handle anticipated errors.
  • Graceful Handling: Provide meaningful fallback or error messages to aid debugging.
  • Resource Management: Ensure resources like files are properly closed, even if an error occurs.

Raising Errors

Sometimes, it's necessary to raise exceptions intentionally to signal that an error condition has occurred.

The raise Statement

Use the raise keyword to trigger an exception when a condition isn't met.

Syntax

if error_condition:
raise ExceptionType("Error message")

Example: Validating Function Arguments

def validate_user(username, minlen):
if minlen < 1:
raise ValueError("minlen must be at least 1")
if len(username) < minlen:
return False
if not username.isalnum():
return False
return True
  • Input Validation: Checks if minlen is at least 1.
  • Raise Exception: Uses raise ValueError to signal invalid minlen.
  • Function Logic: Continues with username validation if inputs are valid.

Using Assertions

The assert statement is used to test conditions that should always be true during execution.

Syntax

assert condition, "Error message"

Example: Type Checking with Assertions

def validate_user(username, minlen):
assert type(username) == str, "username must be a string"
if minlen < 1:
raise ValueError("minlen must be at least 1")
# Additional validation...
  • Assertion: Ensures username is a string.
  • Error Message: Provides a custom message if the assertion fails.
  • Note: Assertions can be disabled globally, so they're best used for debugging rather than input validation.

When to Use Raise vs. Assert

  • raise: Use when handling expected error conditions that require action.
  • assert: Use for debugging purposes to check for conditions that should never occur.

Testing for Expected Errors

Testing your code includes verifying that it properly handles error conditions.

Using unittest to Test Exceptions

The unittest module provides methods to test for exceptions.

Example: Testing Exception Raising

import unittest
from validations import validate_user

class TestValidateUser(unittest.TestCase):
def test_valid(self):
self.assertEqual(validate_user("validuser", 3), True)

def test_too_short(self):
self.assertEqual(validate_user("inv", 5), False)

def test_invalid_characters(self):
self.assertEqual(validate_user("invalid_user", 1), False)

def test_invalid_minlen(self):
self.assertRaises(ValueError, validate_user, "user", -1)

if __name__ == '__main__':
unittest.main()
  • assertRaises: Checks that validate_user raises a ValueError when minlen is invalid.
  • Test Cases: Include both valid and invalid inputs to thoroughly test the function.

Running the Tests

Execute the test script:

python validations_test.py

Expected Output:

....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK
  • Indicates all tests passed successfully.

Summary

  • Exception Handling: Use try-except blocks to catch and handle exceptions.
  • Raising Exceptions: Employ raise to signal errors when input conditions aren't met.
  • Assertions: Use assert statements to enforce conditions during development.
  • Testing Exceptions: Utilize unittest 's assertRaises to ensure functions raise expected exceptions.

By effectively handling exceptions and testing for error conditions, you create robust programs that can handle unexpected situations gracefully.

Code Examples

character_frequency. Py

#!/usr/bin/env python3

def character_frequency(filename):
"""Counts the frequency of each character in the given file."""
# First try to open the file
try:
f = open(filename)
except OSError:
return None
# Now process the file
characters = {}
for line in f:
for char in line:
characters[char] = characters.get(char, 0) + 1
f.close()
return characters

Validations. Py

#!/usr/bin/env python3

def validate_user(username, minlen):
assert type(username) == str, "username must be a string"
if minlen < 1:
raise ValueError("minlen must be at least 1")
if len(username) < minlen:
return False
if not username.isalnum():
return False
return True

validations_test. Py

#!/usr/bin/env python3

import unittest
from validations import validate_user

class TestValidateUser(unittest.TestCase):
def test_valid(self):
self.assertEqual(validate_user("validuser", 3), True)

def test_too_short(self):
self.assertEqual(validate_user("inv", 5), False)

def test_invalid_characters(self):
self.assertEqual(validate_user("invalid_user", 1), False)

def test_invalid_minlen(self):
self.assertRaises(ValueError, validate_user, "user", -1)

if __name__ == '__main__':
unittest.main()

Additional Notes

  • Exception Types: Familiarize yourself with built-in exceptions like TypeError, ValueError, OSError, etc.
  • Custom Exceptions: You can define custom exception classes by inheriting from Exception.
  • Clean-Up Actions: Use finally blocks to execute code regardless of whether an exception occurred, useful for resource clean-up.